"use client"; /** * /w/:workspace/settings/members — full member management (the core sidebar * deep-links here). * * - Invite by email → POST /workspaces/:id/members/invites { email, role } * - Change a role → PATCH /workspaces/:id/members/:userId { role } * - Remove a member → DELETE /workspaces/:id/members/:userId * * All operations go through hooks/use-members.ts (single shared implementation, * UPPERCASE wire roles — see that file's header for the core-casing conflict). * Only owner/admin reach this surface (server-side enforcement is authoritative). */ import { useState } from "react"; import { useParams } from "next/navigation"; import { Loader2, Mail, Trash2 } from "lucide-react"; import { toast } from "@/stores/ui-store"; import { useAuth } from "@/hooks/use-auth"; import { useWorkspace, useWorkspaceMembers } from "@/hooks/use-workspace"; import { useInviteMember, useRemoveMember, useUpdateMemberRole, ASSIGNABLE_ROLES, roleLabel } from "@/hooks/use-members"; import { WorkspaceMember } from "@/lib/types"; import { SettingsShell, Section, inputCls } from "@/components/settings/settings-shell"; import { UserAvatar } from "@/components/database/cell-renderer"; export default function SettingsMembersPage() { const params = useParams<{ workspace: string }>(); const slugOrId = params?.workspace ?? ""; const { data: ws, isLoading } = useWorkspace(slugOrId); const members = useWorkspaceMembers(ws?.id ?? ""); const updateRole = useUpdateMemberRole({ workspaceId: ws?.id ?? "" }); const remove = useRemoveMember({ workspaceId: ws?.id ?? "" }); const { user } = useAuth(); const [email, setEmail] = useState(""); const [inviteRole, setInviteRole] = useState("MEMBER"); const invite = useInviteMember({ workspaceId: ws?.id ?? "" }); const role = (ws?.role ?? "").toLowerCase(); const canManage = role === "owner" || role === "admin"; const isOwner = role === "owner"; const submitInvite = () => { if (!email.trim()) return; invite.mutate( { email: email.trim(), role: inviteRole as (typeof ASSIGNABLE_ROLES)[number] }, { onSuccess: () => { toast("success", "Invitation sent", email.trim()); setEmail(""); }, onError: (e) => toast("error", "Invite failed", (e as Error).message) } ); }; const changeRole = (userId: string, next: string) => { updateRole.mutate( { userId, role: next as (typeof ASSIGNABLE_ROLES)[number] }, { onSuccess: () => toast("success", "Role updated"), onError: (e) => toast("error", "Role change failed", (e as Error).message) } ); }; const removeMember = (m: WorkspaceMember) => { if (!window.confirm(`Remove ${m.user.name} from this workspace?`)) return; remove.mutate(m.userId, { onSuccess: () => toast("success", "Member removed"), onError: (e) => toast("error", "Remove failed", (e as Error).message) }); }; return ( {isLoading || !ws ? (

Loading…

) : (
{/* invite */} {canManage && (
setEmail(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submitInvite()} />

Invitations are emailed to the person. They become a member when they accept.

)} {/* members list */}
{members.isLoading &&

Loading…

}
    {(members.data ?? []).map((m) => { const mRole = (m.role ?? "").toUpperCase(); const isSelf = m.userId === user?.id; // Owner role is immutable here (transfer only). Admins can manage // non-owners; owners can manage anyone except themselves. const canEditThis = canManage && mRole !== "OWNER" && (!isOwner || !isSelf); return (
  • {m.user.name} {isSelf ? (you) : null}

    {m.user.email}

    {canEditThis ? ( ) : ( {roleLabel(mRole)} )} {canEditThis && ( )}
  • ); })}
)}
); }